#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# List a ZX81 program.

# Written by Pedro Gimeno Fortea. Donated to the public domain.

import sys

chars = [
  ' ',
  '▘', # Top Left
  '▝', # Top Right
  '▀', # Top
  '▖', # Bottom Left
  '▌', # Left
  '▞', # Top Right & Bottom Left
  '▛', # Inverse Bottom Right
  '▒', # Checkerboard 50% grey
  '🮎', # Half Checkerboard Up
  '🮏', # Half Checkerboard Down
  '"', '£', '$', ':', '?', '(', ')', '>', '<', '=', '+', '-', '*', '/', ';',
  ',', '.',
  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
  'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
  'RND', 'INKEY$', 'PI',
  '{x43}', '{x44}', '{x45}', '{x46}', '{x47}', '{x48}', '{x49}', '{x4A}',
  '{x4B}', '{x4C}', '{x4D}', '{x4E}', '{x4F}', '{x50}', '{x51}', '{x52}',
  '{x53}', '{x54}', '{x55}', '{x56}', '{x57}', '{x58}', '{x59}', '{x5A}',
  '{x5B}', '{x5C}', '{x5D}', '{x5E}', '{x5F}', '{x60}', '{x61}', '{x62}',
  '{x63}', '{x64}', '{x65}', '{x66}', '{x67}', '{x68}', '{x69}', '{x6A}',
  '{x6B}', '{x6C}', '{x6D}', '{x6E}', '{x6F}',
  '⇧', '⇩', '⇦', '⇨', '{GRAPH}', '{EDIT}',
  '↲',
  '⌫', '{K/L MODE}', '{FUNCTION}',
  '{x7A}', '{x7B}', '{x7C}', '{x7D}', '{#}', '{CURSOR}',
  '█', '▟', '▙', '▄', '▜', '▐', '▚', '▗', '🮐','🮒','🮑',
  '{inv"}', '{inv£}', '{inv$}', '{inv:}', '{inv?}', '{inv(}', '{inv)}',
  '{inv>}', '{inv<}', '{inv=}', '{inv+}', '{inv-}', '{inv*}', '{inv/}',
  '{inv;}', '{inv,}', '{inv.}', '{inv0}', '{inv1}', '{inv2}', '{inv3}',
  '{inv4}', '{inv5}', '{inv6}', '{inv7}', '{inv8}', '{inv9}', '{invA}',
  '{invB}', '{invC}', '{invD}', '{invE}', '{invF}', '{invG}', '{invH}',
  '{invI}', '{invJ}', '{invK}', '{invL}', '{invM}', '{invN}', '{invO}',
  '{invP}', '{invQ}', '{invR}', '{invS}', '{invT}', '{invU}', '{invV}',
  '{invW}', '{invX}', '{invY}', '{invZ}',
  '""', 'AT', 'TAB', '{xC3}', 'CODE', 'VAL', 'LEN', 'SIN', 'COS', 'TAN',
  'ASN', 'ACS', 'ATN', 'LN', 'EXP', 'INT', 'SQR', 'SGN', 'ABS', 'PEEK',
  'USR', 'STR$', 'CHR$', 'NOT', '**', 'OR', 'AND', '<=', '>=', '<>', 'THEN',
  'TO', 'STEP', 'LPRINT', 'LLIST', 'STOP', 'SLOW', 'FAST', 'NEW', 'SCROLL',
  'CONT', 'DIM', 'REM', 'FOR', 'GOTO', 'GOSUB', 'INPUT', 'LOAD', 'LIST',
  'LET', 'PAUSE', 'NEXT', 'POKE', 'PRINT', 'PLOT', 'RUN', 'SAVE', 'RAND',
  'IF', 'CLS', 'UNPLOT', 'CLEAR', 'RETURN', 'COPY'
  ]

# Without bytearray, this is needed:
#if sys.hexversion < 0x03000000:
#    print("NOTE: Python 3 required")
# Probably because Python3 treats bytes[i] as int, while Python2 treats
# it as bytes, while both treat bytearray[i] as int.

if len(sys.argv) < 2:
    print("Usage: %s <filename> [hilite]" % sys.argv[0])
    sys.exit(0)

f = open(sys.argv[1], 'rb')
outf = sys.stdout

hilite = False
inv = ''
bold = ''
norm = ''
if len(sys.argv) > 2:
    # Bold tokens, reverse video for inverse chars.
    # Requires terminal supporting ISO-6429 subset.
    # See console_codes(4)
    hilite = True
    inv = '\x1B[7m'
    bold = '\x1B[1m'
    norm = '\x1B[0m'
    for i in range(0x40):
        chars[i+0x80] = inv + chars[i] + norm

try:

    # Both work in Python 3. Only the second lets the rest of the
    # program work in Python 2.
#    buf = f.read(65556)
    buf = bytearray(f.read(65556))

    lineptr = 0x74
    LeadingSpc = False
    while lineptr < len(buf):
        if buf[lineptr] >= 64:
            break
        linenum = buf[lineptr]*256 + buf[lineptr+1]
        linelen = buf[lineptr+2] + 256*buf[lineptr+3]
        lineptr += 4
        if linenum > 9999:
            outf.write(chars[linenum//1000+28] + str(linenum)[-3:] + ' ')
        else:
            outf.write('%4d ' % linenum)
        charptr = lineptr
        lineptr += linelen
        LeadingSpc = False # Represents the opposite of bit 0 of FLAGS
        while True:
            c = buf[charptr]
            if charptr+1 == lineptr:
                if c != 118:
                    outf.write(chars[c])
                    outf.write('{NO EOL}')
                break
            if c == 126: # FP number
                charptr += 6
                continue
            if c & 64: # a token?
                # Carry from the TOKEN-ADD (L0975) subroutine is the key to
                # knowing whether to obey LeadingSpc (see TOKENS, L094B).
                # Returns C if bit 0 of FLAGS should be obeyed.
                # TOKEN-ADD maps C0-FF to 0-3F, then:
                #   if result >= 0x43: no leading space (097F)
                #   else if result >= 0x40: no leading space (0985, C set)
                #   else if result < 0x18: no leading space (0990)
                #   else if first token char < 0x1C: no leading space (0998)
                #   else obey the leading space flag
                # If result < 0x18 then it originally was < 0xD8, so the
                # above rules translate to: obey only if c >= 0xD8 and the
                # token begins with alphanumeric characters.
                if c >= 0xD8 and ('0' <= chars[c] <= '9'
                                 or 'A' <= chars[c] <= 'Z'
                                ) and LeadingSpc:
                    outf.write(' ') # Leading space
                    LeadingSpc = False
                outf.write(bold + chars[c] + norm)
            else:
                outf.write(chars[c])
            if c:
                # Unless we just printed a space, next token should have a
                # leading one
                LeadingSpc = True
            # Trailing space
            if buf[charptr] >= 192: # RND, INKEY$, PI have no trailing space
                c = chars[buf[charptr]][0]
                # Check if alphanum or $ (this skips ** "" >= etc)
                if c == '$' or '0' <= c <= '9' or 'A' <= c <= 'Z':
                    outf.write(' ')
                    LeadingSpc = False
            charptr += 1

        outf.write('\n')
finally:
    f.close()
